Resolving package versions...
No Changes to `/workspaces/Kalman filtering and smoothing/Project.toml`
No Changes to `/workspaces/Kalman filtering and smoothing/Manifest.toml`
Resolving package versions...
No Changes to `/workspaces/Kalman filtering and smoothing/Project.toml`
No Changes to `/workspaces/Kalman filtering and smoothing/Manifest.toml`
Resolving package versions...
No Changes to `/workspaces/Kalman filtering and smoothing/Project.toml`
No Changes to `/workspaces/Kalman filtering and smoothing/Manifest.toml`
Resolving package versions...
No Changes to `/workspaces/Kalman filtering and smoothing/Project.toml`
No Changes to `/workspaces/Kalman filtering and smoothing/Manifest.toml`
Resolving package versions...
No Changes to `/workspaces/Kalman filtering and smoothing/Project.toml`
No Changes to `/workspaces/Kalman filtering and smoothing/Manifest.toml`
Pkg.status()
Status `/workspaces/Kalman filtering and smoothing/Project.toml`
[6e4b80f9] BenchmarkTools v1.8.0
[31c24e10] Distributions v0.25.131
[b964fa9f] LaTeXStrings v1.4.1
[91a5bcdd] Plots v1.41.7
⌅ [86711068] RxInfer v3.10.1
[860ef19b] StableRNGs v1.0.4
Info Packages marked with ⌅ have new versions available but compatibility constraints restrict them from upgrading. To see why use `status --outdated`
Kalman filtering and smoothing (Part 2)
This is an analysis of the RxInfer example at https://examples.rxinfer.com/categories/basic_examples/kalman_filtering_and_smoothing/
Some symbols have been changed
Some content has been added/modified
The preference is to make the math and code names align as much as possible
have a boldface e.g. \(\mathbf{x}\) or \(\boldsymbol{x}\) or \(\mathbf{A}\) or \(\mathbf{\mathbb{X}}\)
code
have a underscore prefix e.g. _x or _A or _𝕏
small case / cap case / blackboard case are generally used to discriminate between vectors, matrices, and cuboids
start of alphabet / end of alphabet are generally used to discriminate between ‘system’ and ‘signal’
Time structure identifiers (i.e. time sequence/series)
math
have a colon subscript e.g. \(x_:\) or \(x_{0:n}\) or \(x_{:n}\)
code
have a ː (length mark) suffix e.g. xː
External (i.e. true, environment) states and parameters identifiers
math
have a superscript * e.g. \(v^*\)
code
have a superscript x e.g. vˣ
the ‘x’ in the code superscript is used to imitate the * superscript in the math
In the following set of examples the goal is to estimate hidden states of a Dynamical process where all hidden states are Gaussians.
We start our journey with a simple
multivariate Linear Gaussian State Space Model (LGSSM), which can be solved analytically. We then solve an
identification problem which does not have an analytical solution. Utimately, we show how RxInfer.jl can
deal with missing observations.
2 System Identification Problem
In this example we are going to attempt to run Bayesian inference and decouple two random-walk signals, which were combined into a single through some deterministic function f. We do not have access to the real values of these signals, but only to their combination. First, we create the sim_batch_data function that accepts f as an argument.
The function returns the real signals \(x^*_1\) and \(x^*_2\) for later comparison (we are not going to use them during inference) and their combined version \(y\) (we are going to use it as our observations during the inference). We also assume that \(y\) is corrupted with some measurement noise.
Combination 1: \(y = x^*_1 + x^*_2\)
In our first example, we are going to use a simple addition (+) as the function f. In general, it is impossible to decouple the signals \(x^*_1\) and \(x^*_2\) without strong priors, but we can try and see how good an inference can be. The + operation on two random variables also has a special meaning in the probabilistic inference, namely the convolution of pdf’s of the two random variables, and RxInfer treats it specially with many precomputed analytical rules, which may make the inference task easier. First, let us create a test dataset:
xˣ₁₍ₜ₋₁₎ =-20.0## x star 1 at t-1 (not done here as a component of a vector xˣ)xˣ₂₍ₜ₋₁₎ =20.0## x star 2 at t-1 (not done here as a component of a vector xˣ)λˣ_x₁ =0.1λˣ_x₂ =1.0σˣ²ᵥ =20.0## observation noiseT =250
250
The Generative Process
State transition function (\(f_E\))
The state transition function provides the deterministic part of the state flow. The probabilistic part is provided by the system noise:
## Data comes from either a simulation/lab (sim|lab) OR from the field (fld)## Data are handled either in batches (batch) OR online as individual points (point)## Batch data accumulates either## along the depth/examples dimension/axis (into the screen/page), OR## typical for supervised & unsupervised learning## along the time dimension/axis (down the screen page)## typical for sequential decision learning (reinforcement learning & active inference)functionsim_batch_data(f, T; seed=123, xˣ₁₍ₜ₋₁₎, xˣ₂₍ₜ₋₁₎, λˣ_x₁, λˣ_x₂, σˣ²ᵥ) rng =StableRNG(seed) fE₁ː =Vector{Float64}(undef, T) xˣ₁ː =Vector{Float64}(undef, T) fE₂ː =Vector{Float64}(undef, T) xˣ₂ː =Vector{Float64}(undef, T) gEː =Vector{Float64}(undef, T) yː =Vector{Float64}(undef, T)for t in1:T fE₁ː[t] =fE(xˣₜ₋₁=xˣ₁₍ₜ₋₁₎) xˣ₁ː[t] =rand(rng, Normal(fE₁ː[t], sqrt(1.0/λˣ_x₁))) fE₂ː[t] =fE(xˣₜ₋₁=xˣ₂₍ₜ₋₁₎) xˣ₂ː[t] =rand(rng, Normal(fE₂ː[t], sqrt(1.0/λˣ_x₂))) gEː[t] =gE(f=f, xˣ₁ₜ=xˣ₁ː[t], xˣ₂ₜ=xˣ₂ː[t]) yː[t] =rand(rng, Normal(gEː[t], sqrt(σˣ²ᵥ))) xˣ₁₍ₜ₋₁₎ = xˣ₁ː[t] xˣ₂₍ₜ₋₁₎ = xˣ₂ː[t]endreturn xˣ₁ː, xˣ₂ː, yːend
To run inference, we need to create a probabilistic model: our beliefs about how our data could have been generated. For this we can use the @model macro from RxInfer.jl:
RxInfer runs Bayesian inference as a variational optimisation procedure between the real solution and its variational proxy q. In our model specification we assumed noise components to be unknown, thus, we need to enforce a structured mean-field assumption for the variational family of distributions q. This inevitably reduces the accuracy of the result, but makes the task easier and allows for fast and analytical message passing-based variational inference:
The inference results are not so bad, even though RxInfer missed the correct values of the signals between 100 and 150.
Combination 2: \(y = \mathrm{min}(x^*_1, x^*_2)\)
In this example we use a slightly more complex function, for which RxInfer does not have precomputed analytical message update rules. We are going to attempt to run Bayesian inference with min as a combination function. Note, however, that directly using min may cause problems for the built-in approximation methods as it has zero partial derviates with respect to all but one of the variables.
The Generative Process
xˣ₁₍ₜ₋₁₎ =0.0## x star 1 at t-1 (not done here as a component of a vector xˣ)xˣ₂₍ₜ₋₁₎ =0.0## x star 2 at t-1 (not done here as a component of a vector xˣ)λˣ_x₁ =1.0λˣ_x₂ =1.0σˣ²ᵥ =1.0## observation noiseT =200
We generate data with the min function directly, however we model it with a somewhat smoothed version:
## Smoothed version of `min` without zero-ed derivativesfunctionsmooth_min(x, y) if x < yreturn x +1e-4*yelsereturn y +1e-4*xendend
smooth_min (generic function with 1 method)
RxInfer supports arbitrary nonlinear functions, but it requires an explicit approximation method specification. That can be achieved with the built-in @meta macro:
min_meta =@metabegin## In this example we are going to use a simple `Linearization` methodsmooth_min() ->Linearization()end
As we can see inference with the min function is significantly harder. Even though the combined signal has been inferred with high precision the underlying v and w signals are barely inferred. This may be expected, since the min function essentially destroy the information about one of the signals, thus, making it impossible to decouple two seemingly identical random walk signals. The only one inferred signal is the one which is lower and we have no inference information about the signal which is above. It might be possible to infer the states, however, with more informative priors and structural information about two different signals (e.g. if these are not random walks).
Another way to approach this problem is to use online (filtering) inference procedure from RxInfer, but for that we also need to modify our model specification a bit:
The Generative Process
Next step is to generate our dataset and to run the actual inference procedure! For that we use the infer function with autoupdates keyword:
xˣ₁₍ₜ₋₁₎ =1.0## x star 1 at t-1 (not done here as a component of a vector xˣ)xˣ₂₍ₜ₋₁₎ =-1.0## x star 2 at t-1 (not done here as a component of a vector xˣ)λˣ_x₁ =1.0λˣ_x₂ =1.0σˣ²ᵥ =1.0## observation noiseT =300
Another way to approach to this problem is to use online (filtering) inference procedure from RxInfer, but for that we also need to modify our model specification a bit:
@modelfunctionrx_identification(f, y, m₁₀, λ₁₀, a₁, b₁, m₂₀, λ₂₀, a₂, b₂, a_y, b_y)## We are going to continuosly update our priors## based on new posteriors x₁₀ ~Normal(mean=m₁₀, precision=λ₁₀) λ_x₁ ~Gamma(shape=a₁, rate=b₁) x₂₀ ~Normal(mean=m₂₀, precision=λ₂₀) λ_x₂ ~Gamma(shape=a₂, rate=b₂) λ_y ~Gamma(shape=a_y, rate=b_y) x₁ ~Normal(mean= x₁₀, precision= λ_x₁) x₂ ~Normal(mean = x₂₀, precision = λ_x₂) z ~f(x₁, x₂) y ~Normal(mean=z, precision=λ_y)end
We impose structured mean-field assumption for this model as well:
Online inference in the RxInfer supports the @autoupdates specification, which tells inference procedure how to update priors based on new computed posteriors:
As previously we need to define the @meta structure that specifies the approximation method for the nonlinear function smooth_min (f in the model specification):
RxInferenceEngine:
Posteriors stream | enabled for (λ_x₂, λ_y, λ_x₁, x₂, z, x₁)
Free Energy stream | enabled
Posteriors history | available for (x₂₀, x₁₀, λ_x₂, λ_y, λ_x₁, x₂, z, x₁)
Free Energy history | available
Enabled events | [ ]
The results are quite similar to the smoothing case and, as we can see, one of the random walk is again in the “disabled” state, does not infer anything and simply increases its variance (which is expected for the random walk).